Skip to content

Strong branching fixings + dual cutoff#922

Open
aliceb-nv wants to merge 15 commits intoNVIDIA:release/26.04from
aliceb-nv:strong-branching-fixing
Open

Strong branching fixings + dual cutoff#922
aliceb-nv wants to merge 15 commits intoNVIDIA:release/26.04from
aliceb-nv:strong-branching-fixing

Conversation

@aliceb-nv
Copy link
Contributor

This PR introduces fixings performed during the strong branching at the root if infeasible branches are detected.

The existing code always performs full strong branching on all fractionals at the root. If infeasibility is detected, we can safely conclude, for free, that:

  • If branching down on x_j is infeasible, we can tighten lb[j] = ceil(x_j*).
  • If branching up on x_j is infeasible, we can tighten ub[j] = floor(x_j*).

We also incorporate the incumbent value into the strong branching cutoffs. This may enable tighter fixings as well if we can prove that certain branches cannot provide better incumbents than the cutoff.

Reduced cost strengthening is also now turned on with the default cuopt_cli settings.

This change introduces a -0.5% improvement in average on the dual bound, and tips qap10 to optimality within 10min on my setup.

Description

Issue

Checklist

  • I am familiar with the Contributing Guidelines.
  • Testing
    • New or existing tests cover these changes
    • Added tests
    • Created an issue to follow-up
    • NA
  • Documentation
    • The documentation is up to date with these changes
    • Added new documentation
    • NA

@aliceb-nv aliceb-nv added this to the 26.04 milestone Mar 3, 2026
@aliceb-nv aliceb-nv requested a review from a team as a code owner March 3, 2026 14:51
@aliceb-nv aliceb-nv requested review from Bubullzz and akifcorduk March 3, 2026 14:51
@aliceb-nv aliceb-nv added non-breaking Introduces a non-breaking change improvement Improves an existing functionality labels Mar 3, 2026
@copy-pr-bot
Copy link

copy-pr-bot bot commented Mar 3, 2026

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@aliceb-nv
Copy link
Contributor Author

/ok to test f95031d

@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 05c5addc-b740-40a2-ac61-044600959008

📥 Commits

Reviewing files that changed from the base of the PR and between c5ebbb7 and baf8f33.

📒 Files selected for processing (1)
  • cpp/src/branch_and_bound/branch_and_bound.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • cpp/src/branch_and_bound/branch_and_bound.cpp

📝 Walkthrough

Walkthrough

Adds pruning for fixed fractional variables; threads incumbent upper bound into strong/trial branching; applies post-strong-branching and reduced-cost bound tightening that may fix variables, re-run advanced-basis root LP solves, or return INFEASIBLE/OPTIMAL/TIME_LIMIT/NUMERICAL statuses.

Changes

Cohort / File(s) Summary
Branch-and-bound core
cpp/src/branch_and_bound/branch_and_bound.cpp
Added prune_fixed_fractional_variables(...). In branch_and_bound_t::solve(...) pass upper_bound_.load() into strong_branching(...), perform bounds-tightening from pc_.strong_branch_down/up (ceil/floor root solution), detect infeasible/both-cutoff cases (return INFEASIBLE/OPTIMAL), run bounds_strengthening(...) when bounds change, prune newly-fixed fractional indices, update original_lp_ bounds, and re-solve root LP with dual_phase2_with_advanced_basis(...), propagating solver statuses.
Reduced-cost strengthening / presolve
cpp/src/branch_and_bound/branch_and_bound.cpp (reduced-cost path)
Replaced ad-hoc fractional removal with prune_fixed_fractional_variables(new_lower,new_upper,settings_,fractional). Apply tightened local new_lower/new_upper, prune fractional based on locals, commit tightened bounds to original_lp_, re-solve root LP with dual_phase2_with_advanced_basis(...). Adjust infeasibility handling to return OPTIMAL when strengthening implies no solution beats incumbent and avoid returning NUMERICAL on that infeasible branch.
Strong-branching / pseudo-costs implementation
cpp/src/branch_and_bound/pseudo_costs.cpp
Added f_t upper_bound parameter to strong_branch_helper, strong_branching, and trial_branching; propagated upper_bound through batch/PDLP paths; set child cut_off = upper_bound + dual_tol; treat dual solve status == CUTOFF as terminal; updated template instantiation and call sites to include upper_bound.
Strong-branching / pseudo-costs header
cpp/src/branch_and_bound/pseudo_costs.hpp
Updated strong_branching(...) declaration to insert new f_t upper_bound parameter before pseudo_costs_t& pc.
Solver settings defaulting
cpp/src/mip_heuristics/solver.cu
Map negative context.settings.reduced_cost_strengthening to 2 when initializing branch_and_bound_settings.reduced_cost_strengthening (enable reduced-cost strengthening by default for negative inputs).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Strong branching fixings + dual cutoff' clearly and specifically summarizes the two main changes: variable fixings during strong branching and incorporation of dual cutoff logic.
Description check ✅ Passed The description is directly related to the changeset, explaining the motivation and benefits of strong branching fixings and dual cutoff incorporation with concrete details about lb/ub tightening.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cpp/src/branch_and_bound/branch_and_bound.cpp`:
- Around line 2488-2497: The bounds_strengthening call mutates original_lp_
without synchronization; wrap the call to bounds_strengthening and any immediate
subsequent mutations/use of original_lp_ (e.g., the bounds vectors and the
following prune_fixed_fractional_variables call) in the mutex_original_lp_ lock
to prevent races with external set_new_solution() — acquire mutex_original_lp_
before calling bounds_strengthening(...) and release it after the related
updates to original_lp_ complete.

In `@cpp/src/branch_and_bound/pseudo_costs.cpp`:
- Around line 85-87: The code currently treats dual::status_t::ITERATION_LIMIT
as a proven bound and calls compute_objective(child_problem, solution.x); remove
ITERATION_LIMIT from that condition so obj is computed only for proven statuses
(dual::status_t::OPTIMAL and dual::status_t::CUTOFF) and ensure any logic that
consumes obj (root fixings / cutoff deductions) is not executed when status ==
dual::status_t::ITERATION_LIMIT; update the condition around compute_objective
in pseudo_costs.cpp (the block handling child_problem, solution.x and status) to
skip objective materialization and subsequent bound/fathom logic for
ITERATION_LIMIT.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a92fd23 and f95031d.

📒 Files selected for processing (4)
  • cpp/src/branch_and_bound/branch_and_bound.cpp
  • cpp/src/branch_and_bound/pseudo_costs.cpp
  • cpp/src/branch_and_bound/pseudo_costs.hpp
  • cpp/src/mip_heuristics/solver.cu

@aliceb-nv
Copy link
Contributor Author

/ok to test c84928b

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cpp/src/branch_and_bound/branch_and_bound.cpp`:
- Around line 2513-2516: The log prints "from propagation" as num_fixed -
num_tightened which can be negative because num_fixed counts fixed fractionals
and num_tightened counts tighten operations; fix by computing an explicit
non-negative propagated count before logging (e.g., int num_from_propagation =
num_fixed - num_tightened; if (num_from_propagation < 0) num_from_propagation =
0) and use that variable in the settings_.log.printf call (reference
settings_.log.printf, num_fixed, num_tightened) so the log never shows a
negative "from propagation" value.
- Around line 2526-2556: The current handling of dual::status_t lp_status (from
dual_phase2_with_advanced_basis) treats any non-OPTIMAL/TIME_LIMIT result as
NUMERICAL; change this to explicitly handle WORK_LIMIT and ITERATION_LIMIT (and
any other non-fatal statuses) instead of mapping them to
mip_status_t::NUMERICAL. Locate the lp_status checks around the root solve
(variable lp_status, compute_objective, root_relax_soln_) and the later re-solve
site, and: 1) add explicit branches for dual::status_t::WORK_LIMIT and
dual::status_t::ITERATION_LIMIT that propagate a corresponding solver_status_
(or return a distinct mip_status_t like WORK_LIMIT/ITERATION_LIMIT) OR use the
current objective (root_objective_) as a valid bound for branching/cutoff logic
before continuing; 2) only treat truly numerical failures as
mip_status_t::NUMERICAL. Ensure set_final_solution/set_solution_at_root behavior
is updated to match the new status paths.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f95031d and c84928b.

📒 Files selected for processing (1)
  • cpp/src/branch_and_bound/branch_and_bound.cpp

Comment on lines +2513 to +2516
settings_.log.printf(
"Strong branching bounds tightening: %d variables fixed (%d from propagation)\n",
num_fixed,
num_fixed - num_tightened);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

num_fixed - num_tightened can go negative in the log output.

Line 2516 mixes different counters (num_fixed = fixed fractionals; num_tightened = bound tighten operations), so the “from propagation” number can become negative and misleading.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/branch_and_bound/branch_and_bound.cpp` around lines 2513 - 2516, The
log prints "from propagation" as num_fixed - num_tightened which can be negative
because num_fixed counts fixed fractionals and num_tightened counts tighten
operations; fix by computing an explicit non-negative propagated count before
logging (e.g., int num_from_propagation = num_fixed - num_tightened; if
(num_from_propagation < 0) num_from_propagation = 0) and use that variable in
the settings_.log.printf call (reference settings_.log.printf, num_fixed,
num_tightened) so the log never shows a negative "from propagation" value.

@github-actions
Copy link

🔔 Hi @anandhkb, this pull request has had no activity for 7 days. Please update or let us know if it can be closed. Thank you!

If this is an "epic" issue, then please add the "epic" label to this issue.
If it is a PR and not ready for review, then please convert this to draft.
If you just want to switch off this notification, then use the "skip inactivity reminder" label.

1 similar comment
@github-actions
Copy link

🔔 Hi @anandhkb, this pull request has had no activity for 7 days. Please update or let us know if it can be closed. Thank you!

If this is an "epic" issue, then please add the "epic" label to this issue.
If it is a PR and not ready for review, then please convert this to draft.
If you just want to switch off this notification, then use the "skip inactivity reminder" label.

@rgsl888prabhu rgsl888prabhu changed the base branch from main to release/26.04 March 19, 2026 16:34
@aliceb-nv
Copy link
Contributor Author

/ok to test b6a8980

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
cpp/src/branch_and_bound/branch_and_bound.cpp (1)

2642-2651: ⚠️ Potential issue | 🟠 Major

Handle the actual dual-simplex termination statuses explicitly in these re-solves.

Everywhere else in this file treats LP infeasibility as dual::status_t::DUAL_UNBOUNDED, so these INFEASIBLE branches are inconsistent, and the fallback else still turns WORK_LIMIT/ITERATION_LIMIT into mip_status_t::NUMERICAL. That can abort after a non-fatal tightened-root solve even though the current bound is still usable in this module.

Based on learnings: "When a solver terminates early with status ITERATION_LIMIT, the current objective value is still a valid (lower) bound for minimization, though looser than the optimum. Treat ITERATION_LIMIT results as safe for strong branching bound tightening, fixings, and cutoff logic in these modules."

Also applies to: 2707-2716

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/branch_and_bound/branch_and_bound.cpp` around lines 2642 - 2651, The
lp re-solve branches must explicitly handle dual-simplex termination codes:
replace checks for dual::status_t::INFEASIBLE with
dual::status_t::DUAL_UNBOUNDED where appropriate (to match the rest of the
file), and add explicit branches for dual::status_t::ITERATION_LIMIT and
dual::status_t::WORK_LIMIT so they are treated as safe (use the current
objective as a valid bound) instead of falling into the generic NUMERICAL path;
for these cases update solver_status_ (e.g., to mip_status_t::TIME_LIMIT or a
dedicated ITERATION_LIMIT status consistent with surrounding code), call
set_final_solution(solution, root_objective_) if the code currently does that
for early-termination cases, and return the corresponding solver_status_ rather
than mip_status_t::NUMERICAL; apply the same changes to the analogous block
around the other occurrence (the block covering the lines referenced 2707-2716).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cpp/src/branch_and_bound/branch_and_bound.cpp`:
- Around line 2603-2606: The code currently treats any infeasibility after
incumbent-based strong-branching (SB) or reduced-cost (RC) tightenings as model
infeasibility; instead, only return mip_status_t::INFEASIBLE when the deduction
is independent of the incumbent (i.e. num_cutoff == 0). Update the blocks that
check `feasible` (the SB branch around the `feasible` variable and the RC
propagation branches that may return NUMERICAL/INFEASIBLE) so that: (1) if the
infeasibility came from incumbent-based tightenings (all RC fixings and SB cases
with num_cutoff > 0) do not return INFEASIBLE/NUMERICAL but treat the outcome as
“no solution beats the incumbent”/incumbent-optimal, ensure the incumbent is
published before exiting; (2) only return INFEASIBLE when num_cutoff == 0 (true
model infeasibility); and (3) apply the same change at the other mentioned sites
(the other `feasible`/RC checks around the SB/RC propagation code paths) so all
exits uniformly collapse incumbent-based infeasible outcomes to
incumbent-optimal rather than reporting model infeasibility.

---

Duplicate comments:
In `@cpp/src/branch_and_bound/branch_and_bound.cpp`:
- Around line 2642-2651: The lp re-solve branches must explicitly handle
dual-simplex termination codes: replace checks for dual::status_t::INFEASIBLE
with dual::status_t::DUAL_UNBOUNDED where appropriate (to match the rest of the
file), and add explicit branches for dual::status_t::ITERATION_LIMIT and
dual::status_t::WORK_LIMIT so they are treated as safe (use the current
objective as a valid bound) instead of falling into the generic NUMERICAL path;
for these cases update solver_status_ (e.g., to mip_status_t::TIME_LIMIT or a
dedicated ITERATION_LIMIT status consistent with surrounding code), call
set_final_solution(solution, root_objective_) if the code currently does that
for early-termination cases, and return the corresponding solver_status_ rather
than mip_status_t::NUMERICAL; apply the same changes to the analogous block
around the other occurrence (the block covering the lines referenced 2707-2716).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c25be81d-7b56-455b-8e49-c4103d86bde7

📥 Commits

Reviewing files that changed from the base of the PR and between c84928b and b6a8980.

📒 Files selected for processing (2)
  • cpp/src/branch_and_bound/branch_and_bound.cpp
  • cpp/src/mip_heuristics/solver.cu
🚧 Files skipped from review as they are similar to previous changes (1)
  • cpp/src/mip_heuristics/solver.cu

@aliceb-nv
Copy link
Contributor Author

/ok to test c5ebbb7

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
cpp/src/branch_and_bound/branch_and_bound.cpp (2)

2665-2668: ⚠️ Potential issue | 🟡 Minor

Handle WORK_LIMIT and ITERATION_LIMIT explicitly in SB re-solve path.

The else branch treats all non-handled statuses as NUMERICAL. Per learnings, ITERATION_LIMIT provides a valid dual bound and WORK_LIMIT is a legitimate termination. Both should propagate their respective MIP statuses rather than being misclassified as numerical failures.

📝 Suggested fix
       } else if (lp_status == dual::status_t::TIME_LIMIT) {
         solver_status_ = mip_status_t::TIME_LIMIT;
         set_final_solution(solution, root_objective_);
         return solver_status_;
+      } else if (lp_status == dual::status_t::WORK_LIMIT) {
+        solver_status_ = mip_status_t::WORK_LIMIT;
+        set_final_solution(solution, root_objective_);
+        return solver_status_;
+      } else if (lp_status == dual::status_t::ITERATION_LIMIT) {
+        // Iteration limit during re-solve - treat as work limit
+        solver_status_ = mip_status_t::WORK_LIMIT;
+        set_final_solution(solution, root_objective_);
+        return solver_status_;
       } else {
         settings_.log.printf("LP re-solve after SB tightening returned status %d\n", lp_status);
         return mip_status_t::NUMERICAL;
       }

Based on learnings: "In cuOpt, the dual simplex method is dual-feasible throughout execution… when status is ITERATION_LIMIT, the current objective is still a valid lower bound."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/branch_and_bound/branch_and_bound.cpp` around lines 2665 - 2668, The
current else branch treats any unhandled lp_status as mip_status_t::NUMERICAL;
instead detect solver statuses WORK_LIMIT and ITERATION_LIMIT (check lp_status
against the solver's WORK_LIMIT and ITERATION_LIMIT constants) and return the
corresponding mip_status_t values (e.g., mip_status_t::WORK_LIMIT and
mip_status_t::ITERATION_LIMIT) while logging via settings_.log.printf; only fall
back to mip_status_t::NUMERICAL for true numerical failures. Ensure you
reference lp_status and replace the single return mip_status_t::NUMERICAL with
an explicit conditional mapping to the proper mip_status_t enums.

2736-2739: ⚠️ Potential issue | 🟡 Minor

Handle WORK_LIMIT and ITERATION_LIMIT explicitly in RC re-solve path.

Same issue as the SB path: the else branch classifies all unhandled statuses as NUMERICAL, but WORK_LIMIT and ITERATION_LIMIT are valid termination conditions that should propagate their respective statuses.

📝 Suggested fix
       } else if (lp_status == dual::status_t::TIME_LIMIT) {
         solver_status_ = mip_status_t::TIME_LIMIT;
         set_final_solution(solution, root_objective_);
         return solver_status_;
+      } else if (lp_status == dual::status_t::WORK_LIMIT) {
+        solver_status_ = mip_status_t::WORK_LIMIT;
+        set_final_solution(solution, root_objective_);
+        return solver_status_;
+      } else if (lp_status == dual::status_t::ITERATION_LIMIT) {
+        solver_status_ = mip_status_t::WORK_LIMIT;
+        set_final_solution(solution, root_objective_);
+        return solver_status_;
       } else {
         settings_.log.printf("LP re-solve after RC tightening returned status %d\n", lp_status);
         return mip_status_t::NUMERICAL;
       }

Based on learnings: "In cuOpt, the dual simplex method is dual-feasible throughout execution… when status is ITERATION_LIMIT, the current objective is still a valid lower bound."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/branch_and_bound/branch_and_bound.cpp` around lines 2736 - 2739, The
else branch in the RC re-solve path currently maps any unhandled lp_status to
mip_status_t::NUMERICAL; change it to explicitly handle WORK_LIMIT and
ITERATION_LIMIT from the LP solver: check lp_status for WORK_LIMIT and return
mip_status_t::WORK_LIMIT, check for ITERATION_LIMIT and return
mip_status_t::ITERATION_LIMIT, otherwise keep the existing log + return
mip_status_t::NUMERICAL (use the same settings_.log.printf context and lp_status
variable in the messages). Ensure you update the branch that contains
settings_.log.printf("LP re-solve after RC tightening returned status %d\n",
lp_status); and the return mip_status_t::NUMERICAL; to perform these explicit
checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@cpp/src/branch_and_bound/branch_and_bound.cpp`:
- Around line 2665-2668: The current else branch treats any unhandled lp_status
as mip_status_t::NUMERICAL; instead detect solver statuses WORK_LIMIT and
ITERATION_LIMIT (check lp_status against the solver's WORK_LIMIT and
ITERATION_LIMIT constants) and return the corresponding mip_status_t values
(e.g., mip_status_t::WORK_LIMIT and mip_status_t::ITERATION_LIMIT) while logging
via settings_.log.printf; only fall back to mip_status_t::NUMERICAL for true
numerical failures. Ensure you reference lp_status and replace the single return
mip_status_t::NUMERICAL with an explicit conditional mapping to the proper
mip_status_t enums.
- Around line 2736-2739: The else branch in the RC re-solve path currently maps
any unhandled lp_status to mip_status_t::NUMERICAL; change it to explicitly
handle WORK_LIMIT and ITERATION_LIMIT from the LP solver: check lp_status for
WORK_LIMIT and return mip_status_t::WORK_LIMIT, check for ITERATION_LIMIT and
return mip_status_t::ITERATION_LIMIT, otherwise keep the existing log + return
mip_status_t::NUMERICAL (use the same settings_.log.printf context and lp_status
variable in the messages). Ensure you update the branch that contains
settings_.log.printf("LP re-solve after RC tightening returned status %d\n",
lp_status); and the return mip_status_t::NUMERICAL; to perform these explicit
checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 24b72020-5f2c-401f-83dd-a2650132a31f

📥 Commits

Reviewing files that changed from the base of the PR and between b6a8980 and c5ebbb7.

📒 Files selected for processing (1)
  • cpp/src/branch_and_bound/branch_and_bound.cpp

@aliceb-nv
Copy link
Contributor Author

/ok to test baf8f33

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Improves an existing functionality non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant